[READ-ONLY] a fast, modern browser for the npm registry
at main 118 lines 4.2 kB view raw
1import type { NpmDownloadCount } from '#shared/types' 2import { 3 CACHE_MAX_AGE_FIVE_MINUTES, 4 ERROR_NPM_FETCH_FAILED, 5 NPM_API, 6} from '#shared/utils/constants' 7import { encodePackageName } from '#shared/utils/npm' 8 9/** 10 * Returns lightweight package metadata for search results. 11 * 12 * Fetches the full packument + weekly downloads server-side, extracts only 13 * the fields needed for package cards, and returns a small JSON payload. 14 * This avoids sending the full packument (which can be MBs) to the client. 15 * 16 * URL patterns: 17 * - /api/registry/package-meta/packageName 18 * - /api/registry/package-meta/@scope/packageName 19 */ 20export default defineCachedEventHandler( 21 async event => { 22 const pkgParam = getRouterParam(event, 'pkg') 23 if (!pkgParam) { 24 throw createError({ statusCode: 404, message: 'Package name is required' }) 25 } 26 27 const packageName = decodeURIComponent(pkgParam) 28 const encodedName = encodePackageName(packageName) 29 30 try { 31 const [packument, downloads] = await Promise.all([ 32 fetchNpmPackage(packageName), 33 $fetch<NpmDownloadCount>(`${NPM_API}/downloads/point/last-week/${encodedName}`).catch( 34 () => null, 35 ), 36 ]) 37 38 const latestVersion = 39 packument['dist-tags']?.latest || Object.values(packument['dist-tags'] ?? {})[0] || '' 40 const modified = packument.time?.modified || packument.time?.[latestVersion] || '' 41 const date = packument.time?.[latestVersion] || modified 42 43 // Extract repository URL from the packument's repository field 44 // TODO: @npm/types says repository is always an object, but some old 45 // packages have a bare string in the registry JSON 46 let repositoryUrl: string | undefined 47 if (packument.repository) { 48 const repo = packument.repository as { url?: string } | string 49 const rawUrl = typeof repo === 'string' ? repo : repo.url 50 if (rawUrl) { 51 // Normalize git+https:// and git:// URLs to https:// 52 repositoryUrl = rawUrl 53 .replace(/^git\+/, '') 54 .replace(/^git:\/\//, 'https://') 55 .replace(/\.git$/, '') 56 } 57 } 58 59 // Extract bugs URL 60 // TODO: @npm/types types bugs as { email?: string; url?: string } on 61 // packuments, but some old packages store it as a plain URL string 62 let bugsUrl: string | undefined 63 if (packument.bugs) { 64 const bugs = packument.bugs as { url?: string } | string 65 bugsUrl = typeof bugs === 'string' ? bugs : bugs.url 66 } 67 68 // Normalize author field to NpmPerson shape 69 // TODO: @npm/types types author as Contact (object), but some old 70 // packages store it as a plain string (e.g. "Name <email>") 71 let author: { name?: string; email?: string; url?: string } | undefined 72 if (packument.author) { 73 const a = packument.author as { name?: string; email?: string; url?: string } | string 74 author = typeof a === 'string' ? { name: a } : { name: a.name, email: a.email, url: a.url } 75 } 76 77 // Normalize license to a string 78 // TODO: @npm/types types license as string, but some old packages use 79 // the deprecated { type, url } object format 80 const license = packument.license 81 ? typeof packument.license === 'string' 82 ? packument.license 83 : (packument.license as { type: string }).type 84 : undefined 85 86 return { 87 name: packument.name, 88 version: latestVersion, 89 description: packument.description, 90 keywords: packument.keywords, 91 license, 92 date, 93 links: { 94 npm: `https://www.npmjs.com/package/${packument.name}`, 95 homepage: packument.homepage, 96 repository: repositoryUrl, 97 bugs: bugsUrl, 98 }, 99 author, 100 maintainers: packument.maintainers, 101 weeklyDownloads: downloads?.downloads, 102 } 103 } catch (error: unknown) { 104 handleApiError(error, { 105 statusCode: 502, 106 message: ERROR_NPM_FETCH_FAILED, 107 }) 108 } 109 }, 110 { 111 maxAge: CACHE_MAX_AGE_FIVE_MINUTES, 112 swr: true, 113 getKey: event => { 114 const pkg = getRouterParam(event, 'pkg') ?? '' 115 return `package-meta:v1:${pkg}` 116 }, 117 }, 118)